<?php

namespace WDB\Wrapper;

use WDB,
    WDB\Exception;

/**
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
class Record extends WDB\BaseObject implements iRecord
{
    /** @var \WDB\Wrapper\iDBTable */

    protected $tableWrapper;
    /** @var \WDB\Wrapper\Field[] */
    protected $fields;
    /** @var bool */
    protected $validateUponSave;
    /** @var bool */
    protected $isNew;
    /** @var int enumeration of WDB\Query\Insert::MODE_* */
    protected $ODKUmode;
    /** @var WDB\Event\Event */
    protected $validateEvent;
    /** @var array */
    protected $validationErrors;
    /** @var array global validation errors not bound to any column*/
    protected $globalValidationErrors = NULL;
    /** @var WDB\Validation\ColumnValidator */
    protected $columnValidator = NULL;
    /** @var int enumeration of iRecord::WM_* */
    protected $writeMode = iRecord::WM_WRITTEN;


    // <editor-fold defaultstate="collapsed" desc="iRecord implementation">
    public function getTable()
    {
        return $this->tableWrapper;
    }

    public function getValidators()
    {
        return $this->validateEvent->toArray();
    }

    public function getValidationErrors()
    {
        return $this->validationErrors;
    }

    public function getGlobalValidationErrors()
    {
        if ($this->globalValidationErrors === NULL)
        {
            $this->globalValidationErrors = array();
            if (is_array($this->validationErrors))
            {
                foreach ($this->validationErrors as $error)
                {
                    if (is_string($error))
                    {
                        $this->globalValidationErrors[] = $error;
                    }
                }
            }
        }
        return $this->globalValidationErrors;
    }

    public function addValidationErrors($errors)
    {
        $this->validationErrors = array_merge($this->validationErrors, $errors);
    }

    public function addValidator(WDB\Validation\iValidator $validator)
    {
        $this->validateEvent->addListener($validator);
    }

    public function removeValidator(WDB\Validation\iValidator $validator)
    {
        return $this->validateEvent->removeListener($validator);
    }

    public function isValidatedUponSave()
    {
        return $this->validateUponSave;
    }

    public function setValidateUponSave($value)
    {
        $this->validateUponSave = (bool) $value;
    }

    public function validate()
    {
        $this->validationErrors = array();
        $this->globalValidationErrors = NULL;
        foreach ($this->fields as $field)
        {
            $field->resetValidationErrors();
        }
        //return true if there is no FALSE (typechecked) in returned values list
        return !in_array(FALSE, $this->validateEvent->raise($this), TRUE);
    }

    /**
     *
     * @return bool
     */
    public function save()
    {
        if ($this->validateUponSave && !$this->validate())
        {
            throw new Exception\ValidationFailed($this->validationErrors);
        }
        if ($this->isNew)
        {
            $result = $this->saveNew();
            $this->isNew = FALSE;
        } else
        {
            $result = $this->saveUpdate();
        }
        foreach ($this->fields as $field)
        {
            $field->saved();
        }
        foreach ($this->getTable()->getColumns() as $column)
        {
            $column->raiseOnSaved($this);
        }
        return $result;
    }

    public function delete()
    {
        if ($this->isNew)
        {
            throw new Exception\InvalidOperation("cannot delete newly created row.");
        }
        $query = new WDB\Query\Delete($this->tableWrapper->tableAnalyzer->identifier, $this->keyMatchCondition());

        //deleting the record - current object should behave as a new record
        $this->isNew = TRUE;

        return $query->run($this->tableWrapper->database)->success;
    }

    public function changed()
    {
        foreach ($this as $fields)
        {
            if ($field->changed)
                return TRUE;
        }
        return FALSE;
    }

    public function written()
    {
        foreach ($this as $field)
        {
            if ($field->written)
                return TRUE;
        }
        return FALSE;
    }

    public function getIdentificationKey()
    {
        if ($this->isNew()) return NULL;
        $val = array();
        foreach ($this->tableWrapper->getPrimaryKey() as $part)
        {
            $val[$part] = $this->fields[$part]->getOriginalValue();
        }
        return $val;
    }

    public function getStringKey()
    {
        if ($this->isNew()) return NULL;
        $key = $this->getIdentificationKey();
        if (count($key) > 1)
        {
            return $this->tableWrapper->serializeKey($key);
        } else
        {
            return current($key);
        }
    }

    public function isUIDetail()
    {
        return $this->tableWrapper->isUIDetail();
    }

    public function isUIEdit()
    {
        return $this->tableWrapper->isUIEdit();
    }

    public function isUIDelete()
    {
        return $this->tableWrapper->isUIDelete();
    }

    public function isNew()
    {
        return $this->isNew;
    }

    public function setWriteMode($mode)
    {
        $this->writeMode = (int) $mode;
    }

    public function getWriteMode()
    {
        return $this->writeMode;
    }

    // </editor-fold>

    /**
     * Get the Column validator for this record (special common validator for column rules)
     *
     * @return WDB\Validation\ColumnValidator
     */
    public function getColumnValidator()
    {
        return $this->columnValidator;
    }

    // <editor-fold defaultstate="collapsed" desc="constructor and factories">

    public function __construct(Structure\RecordInitializer $ri)
    {
        $this->tableWrapper = $ri->table;
        if (!$this->tableWrapper instanceof iDBTable)
        {
            throw new Exception\BadArgument("DB Record class needs a table implementing iDBTable. iTable interface" .
                    "is insufficient.");
        }
        if ($ri instanceof Structure\NewRecordInitializer)
        {
            $this->constructNew($ri);
        } elseif ($ri instanceof Structure\ExistingRecordInitializer)
        {
            $this->constructExisting($ri);
        } else
        {
            throw new Exception\BadArgument('Usupported record initializer argument: ' . getClass($ri));
        }
        $this->validateEvent = new WDB\Event\Event();
        $this->addValidator($this->columnValidator = new WDB\Validation\ColumnValidator());
        $this->table->fetchValidators($this->validateEvent);
        $this->setValidateUponSave(TRUE);
    }

    private function constructNew(Structure\NewRecordInitializer $ri)
    {
        $this->isNew = TRUE;
        $this->ODKUmode = intval($ri->insertMode);
        $this->fields = array();
        foreach ($this->tableWrapper->columns as $column)
        {
            $value = isset($ri->data[$column->name]) ? $ri->data[$column->name] : $column->default;
            $this->fields[$column->name] = $column->createField(true, $value);
        }
    }

    private function constructExisting(Structure\ExistingRecordInitializer $ri)
    {
        $this->isNew = FALSE;
        $this->fields = array();
        foreach ($this->tableWrapper->columns as $column)
        {
            $value = isset($ri->row[$column->name]) ? $ri->row[$column->name] : NULL;
            $this->fields[$column->name] = $column->createField(false, $value, $ri->row);
        }
    }

    // </editor-fold>

    /**
     *
     * @return bool
     */
    protected function saveNew()
    {
        $row = array();
        foreach ($this->fields as $field)
        {
            if ($field->isWritten())
            {
                $field->saveToRow($row);
            }
        }
        $query = new WDB\Query\Insert($this->tableWrapper->getTableAnalyzer()->getIdentifier(), $row, $this->ODKUmode);
        $result = $query->run($this->tableWrapper->getDatabase());
        if (!$result->success) return FALSE;
        $this->isNew = FALSE;
        if ($result->insertId)
        {
            foreach ($this->fields as $field)
            {
                /**@var $column WDB\Wrapper\iColumn*/
                if ($field->getColumn()->isAutoIncrement())
                {
                    $field->setValue($result->insertId);
                }
            }
        }
        return TRUE;
    }

    /**
     *
     * @return bool
     */
    protected function saveUpdate()
    {
        $upd = array();
        foreach ($this->fields as $field)
        {
            if ($this->writeMode == iRecord::WM_WRITTEN && $field->isWritten() ||
                    $this->writeMode == iRecord::WM_CHANGED && $field->isChanged() ||
                    $this->writeMode == iRecord::WM_ALL)
            {
                $field->saveToRow($upd);
            }
        }
        $query = new WDB\Query\Update($this->tableWrapper->getTableAnalyzer()->getIdentifier(), $upd);
        $query->where = $this->keyMatchCondition();
        return $query->run($this->tableWrapper->getDatabase())->success;
    }

    /**
     * get a where condition to identify current record in a table by primary key if available, unique key if PK not available
     * or all columns if no key available.
     *
     * @return \WDB\Query\Element\iCondition
     */
    protected function keyMatchCondition()
    {
        $compare = array();
        $columns = $this->tableWrapper->getColumns();
        $ikey = $this->getIdentificationKey();
        foreach ($ikey as $key=>$val)
        {
            $compare[] = WDB\Query\Element\Compare::Equals(
                            WDB\Query\Element\ColumnIdentifier::create($key), $columns[$key]->valueToDatatype($val)
            );
        }
        return WDB\Query\Element\LogicOperator::lAnd($compare);
    }

    // <editor-fold defaultstate="collapsed" desc="field access, Iterator, ArrayAccess and Countable implementations">
    //generic field access methods
    public function fieldExists($name)
    {
        return isset($this->fields[$name]);
    }

    public function getFieldValue($name)
    {
        if (!$this->fieldExists($name))
        {
            throw new Exception\ColumnNotFound($name);
        }
        $columns = $this->tableWrapper->getColumns();
        return $columns[$name]->getFieldValue($this->fields[$name]->getValue());
    }

    public function setFieldValue($name, $value)
    {
        if (!$this->fieldExists($name))
        {
            throw new Exception\ColumnNotFound($name);
        }
        $columns = $this->tableWrapper->getColumns();
        $this->fields[$name]->setValue($columns[$name]->setFieldValue($value));
    }

    /**
     *
     * @param string column name
     * @return Field
     */
    public function getField($name)
    {
        if (!$this->fieldExists($name))
        {
            throw new Exception\ColumnNotFound($name);
        }
        return $this->fields[$name];
    }

    //
    //delegate \ArrayAccess to result row
    //
    public function offsetExists($offset)
    {
        return $this->fieldExists($offset);
    }

    public function offsetGet($offset)
    {
        return $this->getFieldValue($offset);
    }

    public function offsetSet($offset, $value)
    {
        $this->setFieldValue($offset, $value);
    }

    public function offsetUnset($offset)
    {
        $this->setFieldValue($offset, NULL);
    }

    //
    // \Countable
    //
    public function count()
    {
        return count($this->fields);
    }

    //
    // \Iterator
    //
    public function current()
    {
        return current($this->fields)->getValue();
    }

    public function key()
    {
        return key($this->fields);
    }

    public function next()
    {
        $a = next($this->fields);
        if (is_object($a))
        {
            return $a->getValue();
        } else
        {
            return FALSE; //no more elements
        }
    }

    public function rewind()
    {
        return reset($this->fields);
    }

    public function valid()
    {
        $key = key($this->fields);
        return ($key !== NULL && $key !== FALSE);
    }

    // </editor-fold>
}